package com.dhj.yygh.service.impl;

import com.alibaba.excel.util.CollectionUtils;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.dhj.yygh.model.hosp.BookingRule;
import com.dhj.yygh.model.hosp.Department;
import com.dhj.yygh.model.hosp.Hospital;
import com.dhj.yygh.model.hosp.Schedule;
import com.dhj.yygh.mongo_operation.DepartmentTemplate;
import com.dhj.yygh.mongo_operation.ScheduleRepository;
import com.dhj.yygh.mongo_operation.ScheduleTemplate;
import com.dhj.yygh.service.DepartmentService;
import com.dhj.yygh.service.HospitalService;
import com.dhj.yygh.service.ScheduleService;
import com.dhj.yygh.vo.hosp.BookingScheduleRuleVo;
import com.dhj.yygh.vo.hosp.ScheduleOrderVo;
import com.dhj.yygh.vo.hosp.ScheduleQueryVo;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.joda.time.format.DateTimeFormat;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.data.domain.Page;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

@Service
public class ScheduleServiceImpl implements ScheduleService {

    @Autowired
    private ScheduleTemplate scheduleTemplate;

    @Autowired
    private ScheduleRepository scheduleRepository;

    @Autowired
    private MongoTemplate mongoTemplate;

    @Autowired
    private HospitalService hospitalService;

    @Autowired
    private DepartmentTemplate departmentTemplate;


    /**
     * 保存排班信息
     *
     * @param scheduleMap 参数集合对象
     */
    @Override
    public void saveSchedule(Map<String, Object> scheduleMap) {
        String str = JSONObject.toJSONString(scheduleMap);

        Schedule schedule = JSONObject.parseObject(str, Schedule.class);

        Schedule scheduleExist = scheduleTemplate
                .getScheduleByHoscodeAndHosScheduleId(schedule);

        if (scheduleExist != null) {
            // 做修改操作
            schedule.setId(scheduleExist.getId());
            schedule.setUpdateTime(new Date());
            schedule.setIsDeleted(0);

            scheduleRepository.save(schedule);
            return;
        }

        schedule.setCreateTime(new Date());
        schedule.setUpdateTime(new Date());
        schedule.setIsDeleted(0);

        scheduleRepository.save(schedule);
    }

    /**
     * 医院排班信息分页查询
     *
     * @param page            当前页
     * @param limit           每页记录数
     * @param scheduleQueryVo 条件查询的 vo 对象
     * @return
     */
    @Override
    public Page<Schedule> getPageByHoscode(Integer page, Integer limit, ScheduleQueryVo scheduleQueryVo) {

        Schedule schedule = new Schedule();
        BeanUtils.copyProperties(schedule, schedule);

        // 构造模糊查询条件
        ExampleMatcher matcher = ExampleMatcher.matching()
                // 通过字符串来匹配,匹配模式为 tringMatcher.CONTAINING: 包含
                .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING)
                .withIgnoreCase(true); // 忽略大小写

        // 构造条件
        Example<Schedule> example = Example.of(schedule, matcher);

        Pageable pageable = PageRequest.of(page, limit);

        Page<Schedule> schedulePage = scheduleRepository.findAll(example, pageable);

        return schedulePage;
    }

    /**
     * 删除医院排班信息
     *
     * @param hoscode     医院编号
     * @param hosShcedule 排班编号
     * @return 返回删除的记录数
     */
    @Override
    public Long removeByHoscodeAndHosSchedule(String hoscode, String hosShcedule) {

        Long removeCount = scheduleTemplate.removeScheduleByHoscodeAndHsoSchdule(hoscode, hosShcedule);
        return removeCount;
    }

    /**
     * 根据医院编号，科室编号分页查询排班信息
     *
     * @param page      当前页码
     * @param pageCount 每页记录数
     * @param hoscode   医院编号
     * @param depcode   科室编号
     * @return 返回结果 Map 集合
     */
    @Override
    public Map<String, Object> getScheduleRule(Integer page, Integer pageCount, String hoscode, String depcode) {

        // 构造根据医院编号和科室编号查询排班信息的条件
        Criteria criteria = Criteria.where("hoscode").is(hoscode).and("depcode").is(depcode);

        /*
         根据工作日 workDate 进行分组
         Aggregation: MongoTemptale 提供的分组聚合对象
         */
        Aggregation agg = Aggregation.newAggregation(
                // match: 构造匹配条件
                Aggregation.match(criteria),
                // 指定分组字段
                Aggregation.group("workDate")
                        .first("workDate").as("workDate") // 对分组后的字段起别名
                        .count().as("docCount") // 统计当前科室总的医生号源数量并起别名
                        .sum("reservedNumber").as("reservedNumber") // 对科室总可预约数求和并起别名
                        .sum("availableNumber").as("availableNumber"), // 对科室剩余预约数求和并起别名

                // 排序
                Aggregation.sort(Sort.Direction.DESC, "workDate"),

                /*
                 手动分页
                 */
                // 跳过前面的记录，从当前页记录开始
                Aggregation.skip((page - 1) * pageCount),
                Aggregation.limit(pageCount)// 查询指定条记录
        );

        // 最终查询返回结果为 BookingScheduleRuleVo: 根据页面来专门封装的排班信息 vo 对象
        AggregationResults<BookingScheduleRuleVo> aggregate =
                mongoTemplate.aggregate(agg, Schedule.class, BookingScheduleRuleVo.class);

        List<BookingScheduleRuleVo> mappedResults = aggregate.getMappedResults();

        // 当前科室分组查询后的的总记录数查询
        Aggregation totalAgg = Aggregation.newAggregation(
                Aggregation.match(criteria),
                Aggregation.group("workDate")
        );

        // 查询获取总记录数
        AggregationResults<BookingScheduleRuleVo> aggregate1 =
                mongoTemplate.aggregate(totalAgg, Schedule.class, BookingScheduleRuleVo.class);

        Integer totalNum = aggregate1.getMappedResults().size();

        // 把日期中对应的星期获取出
        for (BookingScheduleRuleVo bookingScheduleRuleVo : mappedResults) {
            Date workDate = bookingScheduleRuleVo.getWorkDate();
            String dayOfWeek = this.getDayOfWeek(new DateTime(workDate));
            // 封装星期到 BookingScheduleRuleVo 对象中
            bookingScheduleRuleVo.setDayOfWeek(dayOfWeek);
        }

        /*
         设置要返回的最终分页数据
         */
        Map<String, Object> resultMap = new HashMap<>();

        resultMap.put("bookingScheduleRuleList", mappedResults); // 当前页按照 workDate 分组后的分页数据集合
        resultMap.put("total", totalNum); // 分组的总记录数，实际上就是分页的总记录数

        /*
         获取医院的名称
         */
        // 用于存放其他基础数据，为了结构明了，所以单独创建一个 Map
        Map<String, Object> baseMap = new HashMap<>();
        String hosname = hospitalService.getHospitalByHoscode(hoscode).getHosname();
        baseMap.put("hosname", hosname);// 设置医院名称
        resultMap.put("baseMap", baseMap);// 放入到最终数据集合

        // 返回最终数据的 Map 集合
        return resultMap;
    }

    /**
     * 根据日期获取周几
     *
     * @param dateTime 日期时间对象
     * @return
     */
    private String getDayOfWeek(DateTime dateTime) {
        String dayOfWeek = "";
        // getDayOfWeek 根据日期时间对象判断星期
        switch (dateTime.getDayOfWeek()) {
            case DateTimeConstants.SUNDAY:
                dayOfWeek = "周日";
                break;
            case DateTimeConstants.MONDAY:
                dayOfWeek = "周一";
                break;
            case DateTimeConstants.TUESDAY:
                dayOfWeek = "周二";
                break;
            case DateTimeConstants.WEDNESDAY:
                dayOfWeek = "周三";
                break;
            case DateTimeConstants.THURSDAY:
                dayOfWeek = "周四";
                break;
            case DateTimeConstants.FRIDAY:
                dayOfWeek = "周五";
                break;
            case DateTimeConstants.SATURDAY:
                dayOfWeek = "周六";
            default:
                break;
        }
        return dayOfWeek;
    }

    /**
     * @param hoscode  医院代码
     * @param depcode  科室代码
     * @param workDate 排班时间
     * @return 返回排班详情
     */
    @Override
    public List<Schedule> getScheduleDetails(String hoscode, String depcode, String workDate) {

        // 根据参数，查询 Schedule 的基本信息
        List<Schedule> scheduleList =
                scheduleRepository.findScheduleByHoscodeAndDepcodeAndWorkDate(
                        hoscode, depcode, new DateTime(workDate).toDate());

        // 遍历 Schedule ，添加其他基本信息，例如医院名称,科室名称，日期对应的星期 等
        for (Schedule schedule : scheduleList) {
            this.setScheduleOtherInfo(schedule);
        }

        return scheduleList;
    }

    /**
     * 设置 schedule 对象中的属性
     *
     * @param schedule
     */
    private void setScheduleOtherInfo(Schedule schedule) {

        // 根据医院代码查询医院信息
        Hospital hospitalByHoscode = hospitalService.getHospitalByHoscode(schedule.getHoscode());
        // 根据科室代码和医院代码查询科室信息
        Department departmentByDepcodeAndHoscode =
                departmentTemplate.getDepartmentByDepcodeAndHoscode(schedule.getDepcode(), schedule.getHoscode());

        // 封装参数
        schedule.getParam().put("hosname", hospitalByHoscode.getHosname());
        schedule.getParam().put("depname", departmentByDepcodeAndHoscode.getDepname());
        schedule.getParam().put("dayOfWeek", this.getDayOfWeek(new DateTime(schedule.getWorkDate())));
    }

    /**
     * 获取可预约的排班数据
     *
     * @param page    当前页
     * @param limit   没有记录数
     * @param hoscode 医院代码
     * @param depcode 科室代码
     * @return 返回结果 Map 集合
     */
    /*
    过程概述
    1.根据医院编号和科室编号查询出医院信息获取出其中的预约规则
    2.通过预约规则获取预约周期的分页数据
    3.通过 hoscode | depcode | 分页数据中的 workDate  聚合统计查询 Schedule 数据库，得到预约周期中，每天对应的预约数据(号源数等)
    4.将上一步的得到的每天预约数据的集合中的 Date 为 key ，预约规则为 value 对数据进行合并(合并成 Map 结构后，只需要通过每天的 Date【key】 即可
      得到当天的预约数据【Value】)
    5.遍历上一步的 Map 集合，因为可能存在当天没有排班的情况，判断设置每个 value 中的值
      包括标识是否有排班、有号源、当天对应的星期等情况
    6.最终封装整个数据到 Map 中
     */
    @Override
    public Map<String, Object> getBookingScheduleRule(Integer page, Integer limit, String hoscode, String depcode) {
        Map<String, Object> resultMap = new HashMap<>();// 返回最终结果 Map 集合

        // 获取预约的规则
        Hospital hospital = hospitalService.getHospitalByHoscode(hoscode);
        BookingRule bookingRule = hospital.getBookingRule();

        // 获取可预约的日期(分页)
        IPage<Date> dataPage = this.getListDate(page, limit, bookingRule);

        List<Date> dateList = dataPage.getRecords();

        /*
         获取可预约日期中科室剩余的预约数
         */
        // 使用 hoscode | depcode | workDate 作为条件查询排班，存在当天没有排班的清空，BookingScheduleRuleVo 会为 null
        Criteria criteria = Criteria.where("hoscode").is(hoscode).and("depcode").is(depcode)
                .and("workDate").in(dateList);

        // 聚合查询条件
        Aggregation agg = Aggregation.newAggregation(
                Aggregation.match(criteria),
                Aggregation
                        .group("workDate")
                        .first("workDate").as("workDate")
                        .count().as("docCount")// 当天就诊医生人数
                        .sum("reservedNumber").as("reservedNumber") // 对当天科室总可预约数求和并起别名
                        .sum("availableNumber").as("availableNumber") // 对当天科室剩余预约数求和并起别名
        );

        AggregationResults<BookingScheduleRuleVo> aggregateResult =
                mongoTemplate.aggregate(agg, Schedule.class, BookingScheduleRuleVo.class);


        // 获取分组后数据的集合
        List<BookingScheduleRuleVo> scheduleVoList = aggregateResult.getMappedResults();

        // 合并分组后的数据,日期为 key ,挂号详情为 value 方便前端取值
        Map<Date, BookingScheduleRuleVo> scheduleVoMap = new HashMap<>();

        // 使用 stream Api 将 scheduleVoList 转化为指定 key : value 的 Map
        if (!CollectionUtils.isEmpty(scheduleVoList)) {
            scheduleVoMap = scheduleVoList.stream()
                    .collect(Collectors.toMap(BookingScheduleRuleVo::getWorkDate, BookingScheduleRuleVo -> BookingScheduleRuleVo));
        }

        // 获取可预约的排班规则
        List<BookingScheduleRuleVo> bookingScheduleRuleVoList = new ArrayList<>();// 用于存放封装后的排班数据
        for (int i = 0; i < dateList.size(); i++) {
            Date date = dateList.get(i);

            // 从 scheduleVoMap 中以日期为 key 获取排班的 value 值
            BookingScheduleRuleVo bookingScheduleRuleVo = scheduleVoMap.get(date);
            // 若当天没有排班
            if (bookingScheduleRuleVo == null) {
                bookingScheduleRuleVo = new BookingScheduleRuleVo();

                //就诊医生人数
                bookingScheduleRuleVo.setDocCount(0);

                //科室剩余预约数 -1 表示无号
                bookingScheduleRuleVo.setAvailableNumber(-1);
            }

            // 设置可预约日期
            bookingScheduleRuleVo.setWorkDate(date);
            bookingScheduleRuleVo.setWorkDateMd(date);

            // 计算当前预约日期对应的星期
            String dayOfWeek = this.getDayOfWeek(new DateTime(date));
            bookingScheduleRuleVo.setDayOfWeek(dayOfWeek);

            //最后一页最后一条记录为即将预约  状态 0:正常 1:即将放号 -1:当天已停止挂号
            if (i == dateList.size() - 1 && page == dataPage.getPages()) {
                bookingScheduleRuleVo.setStatus(1);
            } else {
                bookingScheduleRuleVo.setStatus(0);
            }
            //当天预约如果到了停号时间， 不能预约
            if (i == 0 && page == 1) {
                DateTime stopTime = this.getDateTime(new Date(), bookingRule.getStopTime());
                if (stopTime.isBeforeNow()) {
                    //停止预约
                    bookingScheduleRuleVo.setStatus(-1);
                }
            }
            bookingScheduleRuleVoList.add(bookingScheduleRuleVo);
        }
        // 封装可预约日期规则数据
        resultMap.put("bookingScheduleList", bookingScheduleRuleVoList);
        resultMap.put("total", dataPage.getTotal());

        // 其他基础数据
        Map<String, String> baseMap = new HashMap<>();
        // 医院名称
        baseMap.put("hosname", hospitalService.getHospitalByHoscode(hoscode).getHosname());

        // 科室
        Department department = departmentTemplate.getDepartmentByDepcodeAndHoscode(depcode, hoscode);

        // 大科室名称
        baseMap.put("bigname", department.getBigname());

        // 科室名称
        baseMap.put("depname", department.getDepname());

        // 年月
        baseMap.put("workDateString", new DateTime().toString("yyyy 年 MM 月"));

        // 放号时间
        baseMap.put("releaseTime", bookingRule.getReleaseTime());

        // 停号时间
        baseMap.put("stopTime", bookingRule.getStopTime());

        resultMap.put("baseMap", baseMap);

        return resultMap;
    }


    /**
     * 根据 Schedule id 获取 Sechedule 对象
     *
     * @param id id
     * @return 返回 Schedule 对象
     */
    @Override
    public Schedule getScheduleById(String id) {
        Optional<Schedule> schedule = scheduleRepository.findById(id);

        // 设置其信息
        this.setScheduleOtherInfo(schedule.get());
        return schedule.get();
    }

    /**
     * 根据排班 id 获取预约下单排班数据
     *
     * @param scheduleId 排班 id
     * @return 返回
     */
    @Override
    public ScheduleOrderVo getScheduleOrderVo(String scheduleId) {
        ScheduleOrderVo scheduleOrderVo = new ScheduleOrderVo();

        // 获取封装排班信息
        Schedule schedule = scheduleRepository.findById(scheduleId).get();
        this.setScheduleOtherInfo(schedule);

        // 获取预约规则信息
        Hospital hospital = hospitalService.getHospitalByHoscode(schedule.getHoscode());
        BookingRule bookingRule = hospital.getBookingRule();

        // 将获取的数据设置到 scheduleOrderVo 中
        scheduleOrderVo.setHoscode(schedule.getHoscode());
        scheduleOrderVo.setHosname(hospital.getHosname());
        scheduleOrderVo.setDepcode(schedule.getDepcode());
        scheduleOrderVo.setDepname((String) schedule.getParam().get("depname"));
        scheduleOrderVo.setHosScheduleId(schedule.getHosScheduleId());
        scheduleOrderVo.setAvailableNumber(schedule.getAvailableNumber()); // 剩余号源
        scheduleOrderVo.setTitle(schedule.getTitle()); // 医生职称
        scheduleOrderVo.setReserveDate(schedule.getWorkDate()); // 安排日期(上午\下午)
        scheduleOrderVo.setReserveTime(schedule.getWorkTime()); // 安排时间
        scheduleOrderVo.setAmount(schedule.getAmount()); // 医事服务费

        // 退号截止天数（格式: 医生上班日期 + (退号标识码代表的天数):医院规定每天退号的截至时间; 退号标识码: 就诊前一天值为 -1，当天值为 0）
        int quitDay = bookingRule.getQuitDay();
        // 时间格式: 年 | 月 | 日 | 时 | 分
        DateTime quitTime = this.getDateTime(new DateTime(schedule.getWorkDate()).plusDays(quitDay).toDate(), bookingRule.getQuitTime());
        scheduleOrderVo.setQuitTime(quitTime.toDate());

        // 预约开始时间(格式: 当天日期 : 当天放号时间)
        DateTime startTime = this.getDateTime(new Date(), bookingRule.getReleaseTime());
        scheduleOrderVo.setStartTime(startTime.toDate());

        // 预约截止时间(格式: 当天日期 + 预约周期天数 : 医院规定的每天最晚的停号时间)
        DateTime endTime = this.getDateTime(new DateTime().plusDays(bookingRule.getCycle()).toDate(), bookingRule.getStopTime());
        scheduleOrderVo.setEndTime(endTime.toDate());

        // 当天停止挂号时间(格式: 当天日期 : 医院规定的每天最晚的停号时间)
        DateTime stopTime = this.getDateTime(new Date(), bookingRule.getStopTime());
        scheduleOrderVo.setStartTime(startTime.toDate());

        return scheduleOrderVo;
    }

    // 将放号时间进行分页
    private IPage<Date> getListDate(Integer page, Integer limit, BookingRule bookingRule) {
        // 获取当天的放号时间: 年 | 月 | 日 | 小时 | 分钟
        DateTime dateTime = this.getDateTime(new Date(), bookingRule.getReleaseTime());

        /*
         获取预约的周期(预约周期，预约某项医疗服务需要提前的时间，当过去预约周期时长后，结束当前预约，开始下一轮预约周期)
         例如: 当前 1 月 15 号开始预约，周期 10 天，则在 1 月 25 号之前都可预约
         在当天的预约时间点过后预约的，预约周期推迟一天，到 1 月 26 号之间可预约
         */
        Integer cycle = bookingRule.getCycle();

        // 若当前时间超过放号时间，则周期推迟一天
        if (dateTime.isBeforeNow()) {
            cycle += 1;
        }

        // 每次都获取当天时间到预约周期之间的所有时间集合，每次分页取出集合中的数据
        List<Date> dateList = new ArrayList<>();
        for (int i = 0; i < cycle; i++) {
            // 当前时间往后推迟 i 天
            DateTime dayPlus = new DateTime().plusDays(i);

            // 指定格式
            String dataStr = dayPlus.toString("yyyy-MM-dd");

            dateList.add(new DateTime(dataStr).toDate());
        }

        // 每页显示七天的周期数据
        List<Date> pageDate = new ArrayList<>();
        int start = (page - 1) * limit;// 开始位置
        int end = ((page - 1) * limit) + limit;// 结束位置

        // 若当前页显示的最后一条记录的位置索引大于当前数据记录数
        if (end > dateList.size()) {
            end = dateList.size();// 直接显示 start -> dateList.size() 的所有记录
        }

        // 否则需要分页获取数据
        for (int i = start; i < end; i++) {
            pageDate.add(dateList.get(i));
        }

        IPage<Date> iPage =
                new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, limit, dateList.size());
        iPage.setRecords(pageDate);// 设置当前页面的 Date 数据

        return iPage;
    }

    // 将 Date日期（yyyy-MM-dd HH:mm）转换为 DateTime
    private DateTime getDateTime(Date date, String timeString) {
        // 将日期连接上具体的时间，最终格式化返回 DateTime 类型
        String dateTimeString = new DateTime(date).toString("yyyy-MM-dd") + " " + timeString;
        DateTime dateTime = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm").parseDateTime(dateTimeString);
        return dateTime;
    }

    /**
     * 下单后更新排班数据
     *
     * @param schedule 排班对象
     */
    @Override
    public void updateSchedule(Schedule schedule) {
        scheduleRepository.save(schedule);
    }
}
